/*
 * Javassist, a Java-bytecode translator toolkit.
 * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License.  Alternatively, the contents of this file may be used under
 * the terms of the GNU Lesser General Public License Version 2.1 or later,
 * or the Apache License Version 2.0.
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 */

package com.feilong.lib.javassist;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.security.ProtectionDomain;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Vector;

/**
 * The class loader for Javassist.
 *
 * <p>
 * This is a sample class loader using <code>ClassPool</code>.
 * Unlike a regular class loader, this class loader obtains bytecode
 * from a <code>ClassPool</code>.
 *
 * <p>
 * Note that Javassist can be used without this class loader; programmers
 * can define their own versions of class loader. They can run
 * a program even without any user-defined class loader if that program
 * is statically translated with Javassist.
 * This class loader is just provided as a utility class.
 *
 * <p>
 * Suppose that an instance of <code>MyTranslator</code> implementing
 * the interface <code>Translator</code> is responsible for modifying
 * class files.
 * The startup program of an application using <code>MyTranslator</code>
 * should be something like this:
 *
 * <pre>
 * import javassist.*;
 *
 * public class Main{
 * 
 *     public static void main(String[] args) throws Throwable{
 *         MyTranslator myTrans = new MyTranslator();
 *         ClassPool cp = ClassPool.getDefault();
 *         Loader cl = new Loader(cp);
 *         cl.addTranslator(cp, myTrans);
 *         cl.run("MyApp", args);
 *     }
 * }
 * </pre>
 *
 * <p>
 * Class <code>MyApp</code> is the main program of the application.
 *
 * <p>
 * This program should be executed as follows:
 *
 * <pre>
 * % java Main <i>arg1</i> <i>arg2</i>...
 * </pre>
 *
 * <p>
 * It modifies the class <code>MyApp</code> with a <code>MyTranslator</code>
 * object before the JVM loads it.
 * Then it calls <code>main()</code> in <code>MyApp</code> with arguments
 * <i>arg1</i>, <i>arg2</i>, ...
 *
 * <p>
 * This program execution is equivalent to:
 *
 * <pre>
 * % java MyApp <i>arg1</i> <i>arg2</i>...
 * </pre>
 *
 * <p>
 * except that classes are translated by <code>MyTranslator</code>
 * at load time.
 *
 * <p>
 * If only a particular class must be modified when it is loaded,
 * the startup program can be simpler; <code>MyTranslator</code> is
 * unnecessary. For example, if only a class <code>test.Rectangle</code>
 * is modified, the <code>main()</code> method above will be the following:
 *
 * <pre>
 * ClassPool cp = ClassPool.getDefault();
 * Loader cl = new Loader(cp);
 * CtClass ct = cp.get("test.Rectangle");
 * ct.setSuperclass(cp.get("test.Point"));
 * cl.run("MyApp", args);
 * </pre>
 *
 * <p>
 * This program changes the super class of the <code>test.Rectangle</code>
 * class.
 *
 * <p>
 * <b>Note 1:</b>
 *
 * <p>
 * This class loader does not allow the users to intercept the loading
 * of <code>java.*</code> and <code>javax.*</code> classes (and
 * <code>sun.*</code>, <code>org.xml.*</code>, ...) unless
 * <code>Loader.doDelegation</code> is <code>false</code>. This is because
 * the JVM prohibits a user class loader from loading a system class.
 * Also see Note 2.
 * If this behavior is not appropriate, a subclass of <code>Loader</code>
 * must be defined and <code>loadClassByDelegation()</code> must be overridden.
 *
 * <p>
 * <b>Note 2:</b>
 *
 * <p>
 * If classes are loaded with different class loaders, they belong to
 * separate name spaces. If class <code>C</code> is loaded by a class
 * loader <code>CL</code>, all classes that the class <code>C</code>
 * refers to are also loaded by <code>CL</code>. However, if <code>CL</code>
 * delegates the loading of the class <code>C</code> to <code>CL'</code>,
 * then those classes that the class <code>C</code> refers to
 * are loaded by a parent class loader <code>CL'</code>
 * instead of <code>CL</code>.
 *
 * <p>
 * If an object of class <code>C</code> is assigned
 * to a variable of class <code>C</code> belonging to a different name
 * space, then a <code>ClassCastException</code> is thrown.
 *
 * <p>
 * Because of the fact above, this loader delegates only the loading of
 * <code>javassist.Loader</code>
 * and classes included in package <code>java.*</code> and
 * <code>javax.*</code> to the parent class
 * loader. Other classes are directly loaded by this loader.
 *
 * <p>
 * For example, suppose that <code>java.lang.String</code> would be loaded
 * by this loader while <code>java.io.File</code> is loaded by the parent
 * class loader. If the constructor of <code>java.io.File</code> is called
 * with an instance of <code>java.lang.String</code>, then it may throw
 * an exception since it accepts an instance of only the
 * <code>java.lang.String</code> loaded by the parent class loader.
 *
 * @see com.feilong.lib.javassist.ClassPool
 * @see com.feilong.lib.javassist.Translator
 */
public class Loader extends ClassLoader{

    /**
     * A simpler class loader.
     * This is a class loader that exposes the protected {@code defineClass()} method
     * declared in {@code java.lang.ClassLoader}. It provides a method similar to
     * {@code CtClass#toClass()}.
     *
     * <p>
     * When loading a class, this class loader delegates the work to the
     * parent class loader unless the loaded classes are explicitly given
     * by {@link #invokeDefineClass(CtClass)}.
     * Note that a class {@code Foo} loaded by this class loader is
     * different from the class with the same name {@code Foo} but loaded by
     * another class loader. This is Java's naming rule.
     * </p>
     *
     * @since 3.24
     */
    public static class Simple extends ClassLoader{

        /**
         * Constructs a class loader.
         */
        public Simple(){
        }

        /**
         * Constructs a class loader.
         * 
         * @param parent
         *            the parent class loader.
         */
        public Simple(ClassLoader parent){
            super(parent);
        }

        /**
         * Invokes the protected {@code defineClass()} in {@code ClassLoader}.
         * It converts the given {@link CtClass} object into a {@code java.lang.Class} object.
         */
        public Class<?> invokeDefineClass(CtClass cc) throws IOException,CannotCompileException{
            byte[] code = cc.toBytecode();
            return defineClass(cc.getName(), code, 0, code.length);
        }
    }

    private HashMap<String, ClassLoader> notDefinedHere;     // must be atomic.

    private Vector<String>               notDefinedPackages; // must be atomic.

    private ClassPool                    source;

    private Translator                   translator;

    private ProtectionDomain             domain;

    /**
     * Specifies the algorithm of class loading.
     *
     * <p>
     * This class loader uses the parent class loader for
     * <code>java.*</code> and <code>javax.*</code> classes.
     * If this variable <code>doDelegation</code>
     * is <code>false</code>, this class loader does not delegate those
     * classes to the parent class loader.
     *
     * <p>
     * The default value is <code>true</code>.
     */
    public boolean                       doDelegation = true;

    /**
     * Creates a new class loader.
     */
    public Loader(){
        this(null);
    }

    /**
     * Creates a new class loader.
     *
     * @param cp
     *            the source of class files.
     */
    public Loader(ClassPool cp){
        init(cp);
    }

    /**
     * Creates a new class loader
     * using the specified parent class loader for delegation.
     *
     * @param parent
     *            the parent class loader.
     * @param cp
     *            the source of class files.
     */
    public Loader(ClassLoader parent, ClassPool cp){
        super(parent);
        init(cp);
    }

    private void init(ClassPool cp){
        notDefinedHere = new HashMap<>();
        notDefinedPackages = new Vector<>();
        source = cp;
        translator = null;
        domain = null;
        delegateLoadingOf(com.feilong.lib.javassist.Loader.class.getName());
    }

    /**
     * Records a class so that the loading of that class is delegated
     * to the parent class loader.
     *
     * <p>
     * If the given class name ends with <code>.</code> (dot), then
     * that name is interpreted as a package name. All the classes
     * in that package and the sub packages are delegated.
     */
    public void delegateLoadingOf(String classname){
        if (classname.endsWith(".")){
            notDefinedPackages.addElement(classname);
        }else{
            notDefinedHere.put(classname, this);
        }
    }

    /**
     * Sets the protection domain for the classes handled by this class
     * loader. Without registering an appropriate protection domain,
     * the program loaded by this loader will not work with a security
     * manager or a signed jar file.
     */
    public void setDomain(ProtectionDomain d){
        domain = d;
    }

    /**
     * Sets the soruce <code>ClassPool</code>.
     */
    public void setClassPool(ClassPool cp){
        source = cp;
    }

    /**
     * Adds a translator, which is called whenever a class is loaded.
     *
     * @param cp
     *            the <code>ClassPool</code> object for obtaining
     *            a class file.
     * @param t
     *            a translator.
     * @throws NotFoundException
     *             if <code>t.start()</code> throws an exception.
     * @throws CannotCompileException
     *             if <code>t.start()</code> throws an exception.
     */
    public void addTranslator(ClassPool cp,Translator t) throws NotFoundException,CannotCompileException{
        source = cp;
        translator = t;
        t.start(cp);
    }

    /**
     * Loads a class and calls <code>main()</code> in that class.
     *
     * @param args
     *            command line parameters.
     *
     *            <br>
     *            &nbsp;&nbsp;{@code args[0]} is the class name to be loaded.
     *            <br>
     *            &nbsp;&nbsp;{@code args[1..n]} are parameters passed
     *            to the target {@code main()}.
     */
    public void run(String[] args) throws Throwable{
        if (args.length >= 1){
            run(args[0], Arrays.copyOfRange(args, 1, args.length));
        }
    }

    /**
     * Loads a class and calls <code>main()</code> in that class.
     *
     * @param classname
     *            the loaded class.
     * @param args
     *            parameters passed to <code>main()</code>.
     */
    public void run(String classname,String[] args) throws Throwable{
        Class<?> c = loadClass(classname);
        try{
            c.getDeclaredMethod("main", new Class<?>[] { String[].class }).invoke(null, new Object[] { args });
        }catch (InvocationTargetException e){
            throw e.getTargetException();
        }
    }

    /**
     * Requests the class loader to load a class.
     */
    @Override
    protected Class<?> loadClass(String name,boolean resolve) throws ClassFormatError,ClassNotFoundException{
        name = name.intern();
        synchronized (name){
            Class<?> c = findLoadedClass(name);
            if (c == null){
                c = loadClassByDelegation(name);
            }

            if (c == null){
                c = findClass(name);
            }

            if (c == null){
                c = delegateToParent(name);
            }

            if (resolve){
                resolveClass(c);
            }

            return c;
        }
    }

    /**
     * Finds the specified class using <code>ClassPath</code>.
     * If the source throws an exception, this returns null.
     *
     * <p>
     * This method can be overridden by a subclass of
     * <code>Loader</code>. Note that the overridden method must not throw
     * an exception when it just fails to find a class file.
     *
     * @return null if the specified class could not be found.
     * @throws ClassNotFoundException
     *             if an exception is thrown while
     *             obtaining a class file.
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException{
        byte[] classfile;
        try{
            if (source != null){
                if (translator != null){
                    translator.onLoad(source, name);
                }

                try{
                    classfile = source.get(name).toBytecode();
                }catch (NotFoundException e){
                    return null;
                }
            }else{
                String jarname = "/" + name.replace('.', '/') + ".class";
                InputStream in = this.getClass().getResourceAsStream(jarname);
                if (in == null){
                    return null;
                }

                classfile = ClassPoolTail.readStream(in);
            }
        }catch (Exception e){
            throw new ClassNotFoundException("caught an exception while obtaining a class file for " + name, e);
        }

        int i = name.lastIndexOf('.');
        if (i != -1){
            String pname = name.substring(0, i);
            if (isDefinedPackage(pname)){
                try{
                    definePackage(pname, null, null, null, null, null, null, null);
                }catch (IllegalArgumentException e){
                    // ignore.  maybe the package object for the same
                    // name has been created just right away.
                }
            }
        }

        if (domain == null){
            return defineClass(name, classfile, 0, classfile.length);
        }
        return defineClass(name, classfile, 0, classfile.length, domain);
    }

    private boolean isDefinedPackage(String name){
        return getPackage(name) == null;
    }

    protected Class<?> loadClassByDelegation(String name) throws ClassNotFoundException{
        /*
         * The swing components must be loaded by a system
         * class loader.
         * javax.swing.UIManager loads a (concrete) subclass
         * of LookAndFeel by a system class loader and cast
         * an instance of the class to LookAndFeel for
         * (maybe) a security reason. To avoid failure of
         * type conversion, LookAndFeel must not be loaded
         * by this class loader.
         */

        Class<?> c = null;
        if (doDelegation){
            if (name.startsWith("java.") || name.startsWith("javax.") || name.startsWith("sun.") || name.startsWith("com.sun.")
                            || name.startsWith("org.w3c.") || name.startsWith("org.xml.") || notDelegated(name)){
                c = delegateToParent(name);
            }
        }

        return c;
    }

    private boolean notDelegated(String name){
        if (notDefinedHere.containsKey(name)){
            return true;
        }

        for (String pack : notDefinedPackages){
            if (name.startsWith(pack)){
                return true;
            }
        }

        return false;
    }

    protected Class<?> delegateToParent(String classname) throws ClassNotFoundException{
        ClassLoader cl = getParent();
        if (cl != null){
            return cl.loadClass(classname);
        }
        return findSystemClass(classname);
    }
}
